/*
 * PDLogger
 * 
 * Copyright (c) 2012, Zoujie Wu <yibn2008@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * 
 *   * Redistributions of source code must retain the above copyright notice, this 
 *     list of conditions and the following disclaimer.
 *   
 *   * Redistributions in binary form must reproduce the above copyright notice, 
 *     this list of conditions and the following disclaimer in the documentation 
 *     and/or other materials provided with the distribution.
 *   
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <time.h>

#include "php.h"
#include "ext/date/php_date.h"
#include "ext/standard/php_string.h"
#include "ext/standard/php_smart_str.h"
#include "ext/standard/php_filestat.h"
#include "ext/standard/html.h"
#include "zend_operators.h"
#include "zend_exceptions.h"
#include "php_pdlogger.h"
#include "pdl_watch.h"
#include "pdl_output.h"

#define smart_str_append_const(str, const) smart_str_appendl(str, const, sizeof(const) - 1)

#define PDL_TRACE_KEY_APPEND(key) \
	do { \
		if (zend_hash_find(ht, key, sizeof(key), (void**) &ztmp) == SUCCESS) { \
			smart_str_appendl(buf, Z_STRVAL_PP(ztmp), Z_STRLEN_PP(ztmp)); \
		} \
	} while (0)


static int pdl_format_record_time(char **str, long sec, long usec TSRMLS_DC);
static int pdl_format_array_items(zval **item TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key);
static int pdl_indent_string(char **str, const char *src, int len, int indent);
static int pdl_format_object(char **str, zval *object, int indent TSRMLS_DC);
static int pdl_format_object_property(zval **prop TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key);
static int pdl_format_exception(char **str, zval *ex, int indent TSRMLS_DC);
static int pdl_format_exception_trace(zval **frame TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key);
static int pdl_format_exception_trace_arg(zval **arg TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key);

static int pdl_gen_xml_log_header(char **header, long date, int period TSRMLS_DC);
static int pdl_record_to_xml_str(pdl_record_t **record_p, smart_str *buf TSRMLS_DC);
static int pdl_gen_text_log_header(char **header, long date, int period TSRMLS_DC);
static int pdl_record_to_text_str(pdl_record_t **record_p, smart_str *buf TSRMLS_DC);

static char* pdl_gen_log_file(zval* base_dir, int format TSRMLS_DC);

/**
 * append spaces to smart_str
 */
static void smart_str_append_spaces(smart_str *buf, int spaces_num)
{
	char *tmp_str;

	/* the "%*c" print at least one char ' ', whatever the spaces_num is */
	if (spaces_num > 0) {
		tmp_str = (char *) emalloc(sizeof(char) * (spaces_num + 1));
		tmp_str[spaces_num] = 0;
		sprintf(tmp_str, "%*c", spaces_num, ' ');
		smart_str_appendl(buf, tmp_str, spaces_num);
		efree(tmp_str);
	}
}

/**
 * format record time to string
 */
static int pdl_format_record_time(char **str, long sec, long usec TSRMLS_DC)
{
	char *date_str;
	int str_len;
	
	date_str = php_format_date(ZEND_STRL(PDL_DATE_FORMAT), sec, 1 TSRMLS_CC);
	str_len = spprintf(str, 0, "%s.%d", date_str, (int) (usec / MICRO_IN_SEC * 1000));
	
	efree(date_str);
	
	return str_len;
}

/**
 * format record data to string
 */
PDL_API int pdl_format_zval(char **str, zval *data, zend_bool with_eol, int indent, int layer TSRMLS_DC)
{
	smart_str	buf = {0};
	int			str_len;
	char 		*tmp_str;
	int			tmp_len;

	if (data) {
		if ((Z_TYPE_P(data) != IS_OBJECT) && (layer == 0)) {
			smart_str_append_spaces(&buf, indent);
		}

		switch (Z_TYPE_P(data)) {
		case IS_NULL:
			smart_str_append_const(&buf, "(null) null");
			break;
		case IS_BOOL:
			if (Z_BVAL_P(data)) {
				smart_str_append_const(&buf, "(bool) true");
			} else {
				smart_str_append_const(&buf, "(bool) false");
			}
			break;
		case IS_LONG:
			tmp_len = spprintf(&tmp_str, 0, "(long) %ld", Z_LVAL_P(data));
			smart_str_appendl(&buf, tmp_str, tmp_len);
			efree(tmp_str);
			break;
		case IS_DOUBLE:
			tmp_len = spprintf(&tmp_str, 0, "(double) %.*f", (int) EG(precision), Z_DVAL_P(data));
			smart_str_appendl(&buf, tmp_str, tmp_len);
			efree(tmp_str);
			break;
		case IS_ARRAY:
			tmp_len = spprintf(&tmp_str, 0, "array[%d] (", zend_hash_num_elements(Z_ARRVAL_P(data)));
			smart_str_appendl(&buf, tmp_str, tmp_len);
			efree(tmp_str);
			if ((zend_hash_num_elements(Z_ARRVAL_P(data)) > 0) && (layer < PDL_MAX_NEST_LAYER)) {
				smart_str_appendc(&buf, '\n');
				zend_hash_apply_with_arguments(Z_ARRVAL_P(data) TSRMLS_CC, (apply_func_args_t) pdl_format_array_items, 3, &buf, indent, layer + 1);
				smart_str_append_spaces(&buf, indent + layer * PDL_FORMAT_INDENT);
			}
			smart_str_appendc(&buf, ')');
			break;
		case IS_OBJECT:
			if (instanceof_function(Z_OBJCE_P(data), zend_exception_get_default(TSRMLS_C) TSRMLS_CC)) {
				tmp_len = pdl_format_exception(&tmp_str, data, indent TSRMLS_CC);
				smart_str_appendl(&buf, tmp_str, tmp_len);
				efree(tmp_str);
			} else {
				tmp_len = pdl_format_object(&tmp_str, data, indent TSRMLS_CC);
				smart_str_appendl(&buf, tmp_str, tmp_len);
				efree(tmp_str);
			}
			break;
		case IS_STRING:
			tmp_len = pdl_indent_string(&tmp_str, Z_STRVAL_P(data), Z_STRLEN_P(data), indent);
			smart_str_appendl(&buf, tmp_str, tmp_len);
			efree(tmp_str);
			break;
		case IS_RESOURCE:
			smart_str_append_const(&buf, "(resource)");
			break;
		default:
			smart_str_append_const(&buf, "(unknown)");
		}
		
		if (with_eol) {
			smart_str_appendc(&buf, '\n');
		}
	}

	smart_str_0(&buf);
	*str = estrndup(buf.c, buf.len);
	str_len = buf.len;
	smart_str_free(&buf);
	
	return str_len;
}

/**
 * Indent string with space
 */
static int pdl_indent_string(char **str, const char *src, int len, int indent)
{
	int		i, j = 0;
	int		str_len = len;
	char	*idt_str, *p;
	int		idt_len = indent + 1;
	int		cp_flag = 0;
	int		cp_num = 0;
	
	*str = (char *) emalloc(sizeof(char) * (str_len + 1));
	p = *str;
	
	idt_str = (char *) emalloc(sizeof(char) * (idt_len + 1));
	if (indent > 0) {
		sprintf(idt_str, "\n%*c", indent, ' ');
	} else {
		idt_str[0] = '\n';
	}
	idt_str[idt_len] = 0;
	
	for (i = 0; i < len; i ++) {
		if (src[i] == '\r') {
			if ((i < len - 1) && (src[i] == '\n')) {
				i ++;
			}
		} else if (src[i] != '\n') {
			cp_flag = 1;
		}
		
		cp_num = (j - 1) + (cp_flag ? 1 : idt_len);
		if (cp_num > str_len) {		/* cp_num contains at least a char, so j need to sub 1 */
			str_len = cp_num + (len - i);
			*str = (char *) erealloc(*str, sizeof(char) * (str_len + 1));
			p = *str;
		}
		
		if (cp_flag) {
			p[j] = src[i];
			j ++;
		} else {
			memcpy(&p[j], idt_str, idt_len);
			j += idt_len;
		}
		cp_flag = 0;
	}
	
	p[str_len] = '\0';
	
	efree(idt_str);
	
	return str_len;
}

/**
 * format array items to string
 */
static int pdl_format_array_items(zval **item TSRMLS_DC, int num_args , va_list args, zend_hash_key *hash_key)
{
	smart_str	*buf;
	int			layer;
	int			indent;
	char		*tmp_str;
	int			tmp_len;
	const char	*class_name;
	int			dup;
	
	buf = va_arg(args, smart_str *);
	indent = va_arg(args, int);
	layer = va_arg(args, int);
	
	smart_str_append_spaces(buf, indent + PDL_FORMAT_INDENT * layer);
	
	smart_str_appendc(buf, '[');
	if (hash_key->nKeyLength == 0) {
		smart_str_append_long(buf, (long) hash_key->h);
	} else {
		tmp_str = php_addcslashes(hash_key->arKey, hash_key->nKeyLength - 1, &tmp_len, 0, "'\\", 2 TSRMLS_CC);
		smart_str_appendc(buf, '\'');
		smart_str_appendl(buf, tmp_str, tmp_len);
		smart_str_appendc(buf, '\'');
		efree(tmp_str);
	}
	smart_str_append_const(buf, "] => ");
	
	if (Z_TYPE_PP(item) == IS_OBJECT) {
		smart_str_append_const(buf, "(object) ");
		dup = zend_get_object_classname(*item, &class_name, &tmp_len TSRMLS_CC);
		smart_str_appendl(buf, class_name, tmp_len);
		if (!dup) {
			efree((char *) class_name);
		}
	} else if (Z_TYPE_PP(item) == IS_STRING) {
		tmp_str = php_addcslashes(Z_STRVAL_PP(item), Z_STRLEN_PP(item), &tmp_len, 0, "'\\", 2 TSRMLS_CC);
		smart_str_append_const(buf, "(string) '");
		smart_str_appendl(buf, tmp_str, tmp_len);
		smart_str_appendc(buf, '\'');
		efree(tmp_str);
	} else {
		tmp_len = pdl_format_zval(&tmp_str, *item, 0, 0, 0 TSRMLS_CC);
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
	}
	
	smart_str_appendl(buf, ",\n", 2);
	
	return ZEND_HASH_APPLY_KEEP;
}

/**
 * format record data (object type) to string
 */
static int pdl_format_object(char **str, zval *object, int indent TSRMLS_DC)
{
	smart_str	buf = {0};
	const char	*class_name;
	int			class_name_len;
	int			str_len;
	HashTable	*propht;
	int			dup;

	smart_str_append_spaces(&buf, indent);
	
	smart_str_append_const(&buf, "object(");
	dup = zend_get_object_classname(object, &class_name, &class_name_len TSRMLS_CC);
	smart_str_appendl(&buf, class_name, class_name_len);
	if (!dup) {
		efree((char *) class_name);
	}
	smart_str_appendl(&buf, ") {\n", 4);

	propht = Z_OBJPROP_P(object);
	if (propht) {
		zend_hash_apply_with_arguments(propht TSRMLS_CC, (apply_func_args_t) pdl_format_object_property, 2, indent, &buf);
	}

	smart_str_append_spaces(&buf, indent);
	smart_str_appendc(&buf, '}');

	smart_str_0(&buf);
	*str = estrndup(buf.c, buf.len);
	str_len = buf.len;
	smart_str_free(&buf);

	return str_len;
}

/**
 * format object property to string
 */
static int pdl_format_object_property(zval **prop TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key)
{
	smart_str	*buf;
	char		*tmp_str;
	int			tmp_len;
	int 		indent;
	const char	*class_name;
	const char	*prop_name;
	int			dup;

	indent = va_arg(args, int);
	buf = va_arg(args, smart_str *);

	smart_str_append_spaces(buf, indent + PDL_FORMAT_INDENT);

	if (hash_key->nKeyLength != 0) {
		zend_unmangle_property_name(hash_key->arKey, hash_key->nKeyLength, &class_name, &prop_name);

		if (class_name) {
			if (class_name[0] == '*') {
				smart_str_append_const(buf, "protected $");
			} else {
				smart_str_append_const(buf, "private $");
			}
		} else {
			smart_str_append_const(buf, "public $");
		}
		smart_str_appends(buf, prop_name);
	} else {
		tmp_len = spprintf(&tmp_str, 0, "[%ld]", hash_key->h);
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
	}

	smart_str_appendl(buf, " = ", 3);

	if (Z_TYPE_PP(prop) == IS_ARRAY) {
		smart_str_append_const(buf, "(array) array[");
		smart_str_append_long(buf, zend_hash_num_elements(Z_ARRVAL_PP(prop)));
		smart_str_appendc(buf, ']');
	} else if (Z_TYPE_PP(prop) == IS_OBJECT) {
		smart_str_append_const(buf, "(object) ");
		dup = zend_get_object_classname(*prop, &class_name, &tmp_len TSRMLS_CC);
		smart_str_appendl(buf, class_name, tmp_len);
		if (!dup) {
			efree((char *) class_name);
		}
	} else if (Z_TYPE_PP(prop) == IS_STRING) {
		tmp_str = php_addcslashes(Z_STRVAL_PP(prop), Z_STRLEN_PP(prop), &tmp_len, 0, "'\\", 2 TSRMLS_CC);
		smart_str_append_const(buf, "(string) '");
		smart_str_appendl(buf, tmp_str, tmp_len);
		smart_str_appendc(buf, '\'');
		efree(tmp_str);
	} else {
		tmp_len = pdl_format_zval(&tmp_str, *prop, 0, 0, 0 TSRMLS_CC);
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
	}

	smart_str_appendl(buf, ";\n", 2);
	
	return ZEND_HASH_APPLY_KEEP;
}

/**
 * format expcetion to string
 */
static int pdl_format_exception(char **str, zval *ex, int indent TSRMLS_DC)
{
	int			str_len;
	char		*tmp_str;
	const char	*class_name;
	int			tmp_len;
	zval		*tmp_zval;
	smart_str	buf = {0};
	int			trace_num = 0;
	int			dup;

	/* Exception */
	smart_str_append_spaces(&buf, indent);
	smart_str_append_const(&buf, "Exception: ");
	dup = zend_get_object_classname(ex, &class_name, &tmp_len TSRMLS_CC);
	smart_str_appendl(&buf, class_name, tmp_len);
	if (!dup) {
		efree((char *) class_name);
	}
	smart_str_appendc(&buf, '\n');

	/* Message */
	smart_str_append_spaces(&buf, indent);
	smart_str_append_const(&buf, "Message: ");
	tmp_zval = zend_read_property(Z_OBJCE_P(ex), ex, ZEND_STRL("message"), 0 TSRMLS_CC);	/* do NOT destroy the tmp_zval, it's a ref of prop */
	smart_str_appendl(&buf, Z_STRVAL_P(tmp_zval), Z_STRLEN_P(tmp_zval));
	smart_str_appendc(&buf, '\n');

	/* Throw At */
	smart_str_append_spaces(&buf, indent);
	smart_str_append_const(&buf, "Throw At: ");
	tmp_zval = zend_read_property(Z_OBJCE_P(ex), ex, ZEND_STRL("file"), 0 TSRMLS_CC);
	smart_str_appendl(&buf, Z_STRVAL_P(tmp_zval), Z_STRLEN_P(tmp_zval));

	smart_str_appendc(&buf, ':');
	tmp_zval = zend_read_property(Z_OBJCE_P(ex), ex, ZEND_STRL("line"), 0 TSRMLS_CC);
	smart_str_append_long(&buf, Z_LVAL_P(tmp_zval));
	smart_str_appendc(&buf, '\n');

	/* Trace */
	tmp_zval = zend_read_property(zend_exception_get_default(TSRMLS_C), ex, ZEND_STRL("trace"), 1 TSRMLS_CC);
	if (Z_TYPE_P(tmp_zval) == IS_ARRAY) {
		smart_str_append_spaces(&buf, indent);
		smart_str_append_const(&buf, "Trace:\n");
		zend_hash_apply_with_arguments(Z_ARRVAL_P(tmp_zval) TSRMLS_CC, (apply_func_args_t) pdl_format_exception_trace, 3, &buf, &trace_num, indent);
		if (indent > 0) {
			tmp_len = spprintf(&tmp_str, 0, "%*c#%d {main}", indent, ' ', trace_num);
		} else {
			tmp_len = spprintf(&tmp_str, 0, "#%d {main}", trace_num);
		}
		smart_str_appendl(&buf, tmp_str, tmp_len);
		efree(tmp_str);
	}

	smart_str_0(&buf);
	*str = estrndup(buf.c, buf.len);
	str_len = buf.len;
	smart_str_free(&buf);

	return str_len;
}

/**
 * format exception trace info to string
 */
static int pdl_format_exception_trace(zval **frame TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key)
{
	smart_str	*buf;
	char		*tmp_str;
	int			tmp_len;
	int			*trace_num;
	int			indent;
	HashTable 	*ht = Z_ARRVAL_PP(frame);
	zval		**file, **ztmp;
	int			line;
	zend_bool	end_comma = 0;

	buf = va_arg(args, smart_str *);
	trace_num = va_arg(args, int*);
	indent = va_arg(args, int);

	/* trace num */
	smart_str_append_spaces(buf, indent);
	tmp_len = spprintf(&tmp_str, 0, "#%d ", (*trace_num)++);
	smart_str_appendl(buf, tmp_str, tmp_len);
	efree(tmp_str);
	
	/* trace file and line */
	if (zend_hash_find(ht, ZEND_STRS("file"), (void**) &file) == SUCCESS) {
		if (zend_hash_find(ht, ZEND_STRS("line"), (void**) &ztmp) == SUCCESS) {
			line = Z_LVAL_PP(ztmp);
		} else {
			line = 0;
		}

		tmp_len = spprintf(&tmp_str, 0, "%s(%d): ", Z_STRVAL_PP(file), line);
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
	} else {
		smart_str_append_const(buf, "[internal function]: ");
	}

	/* trace class + type + function */
	PDL_TRACE_KEY_APPEND("class");
	PDL_TRACE_KEY_APPEND("type");
	PDL_TRACE_KEY_APPEND("function");
	smart_str_appendc(buf, '(');

	/* trace function args */
	if (zend_hash_find(ht, ZEND_STRS("args"), (void**) &ztmp) == SUCCESS) {
		zend_hash_apply_with_arguments(Z_ARRVAL_PP(ztmp) TSRMLS_CC, (apply_func_args_t) pdl_format_exception_trace_arg, 2, buf, &end_comma);
		if (end_comma) {
			buf->len -= 2; /* remove last ', ' */
		}
	}
	smart_str_appendl(buf, ")\n", 2);

	return ZEND_HASH_APPLY_KEEP;
}

/**
 * format exception trace arg
 */
static int pdl_format_exception_trace_arg(zval **arg TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key)
{
	smart_str	*buf;
	zend_bool	*end_comma;
	int			l_added;
	char		*tmp_str;
	const char	*class_name;
	int			tmp_len;
	int			dup;

	buf = va_arg(args, smart_str*);
	end_comma = va_arg(args, zend_bool*);

	/* the trivial way would be to do:
	 * conver_to_string_ex(arg);
	 * append it and kill the now tmp arg.
	 * but that could cause some E_NOTICE and also damn long lines.
	 */

	switch (Z_TYPE_PP(arg)) {
	case IS_NULL:
		smart_str_append_const(buf, "null");
		break;
	case IS_STRING:
		smart_str_appendc(buf, '\'');
		if (Z_STRLEN_PP(arg) > 15) {
			smart_str_appendl(buf, Z_STRVAL_PP(arg), 15);
			smart_str_append_const(buf, "...'");
			l_added = 15 + 4 + 1; /* +1 because of while (--l_added) */
		} else {
			l_added = Z_STRLEN_PP(arg);
			smart_str_appendl(buf, Z_STRVAL_PP(arg), l_added);
			smart_str_appendc(buf, '\'');
			l_added += 1 + 1;
		}

		/* convert the unvisible char to '?' */
		while (--l_added) {
			if (buf->c[buf->len - l_added] < 32) {
				buf->c[buf->len - l_added] = '?';
			}
		}
		break;
	case IS_BOOL:
		if (Z_BVAL_PP(arg)) {
			smart_str_append_const(buf, "true");
		} else {
			smart_str_append_const(buf, "false");
		}
		break;
	case IS_RESOURCE:
		smart_str_append_const(buf, "resource id #");
		/* break; */
	case IS_LONG:
		tmp_len = spprintf(&tmp_str, 0, "%ld", Z_LVAL_PP(arg));
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
		break;
	case IS_DOUBLE:
		/* %G already handles removing trailing zeros from the fractional part, yay */
		tmp_len = spprintf(&tmp_str, 0, "%.*G", (int) EG(precision), Z_DVAL_PP(arg));
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
		break;
	case IS_ARRAY:
		tmp_len = spprintf(&tmp_str, 0, "array[%d]", zend_hash_num_elements(Z_ARRVAL_P(*arg)));
		smart_str_appendl(buf, tmp_str, tmp_len);
		efree(tmp_str);
		break;
	case IS_OBJECT:
		smart_str_append_const(buf, "object(");
		
		dup = zend_get_object_classname(*arg, &class_name, &tmp_len TSRMLS_CC);
		smart_str_appendl(buf, class_name, tmp_len);
		if(!dup) {
			efree((char*)class_name);
		}
		
		smart_str_appendc(buf, ')');
		break;
	}

	smart_str_appendl(buf, ", ", 2);
	*end_comma = 1;

	return ZEND_HASH_APPLY_KEEP;
}

#define HTML_SPECIAL_CHARS(str, str_len, src)	\
	str = php_escape_html_entities_ex(src, strlen(src), &str_len, 0, ENT_COMPAT, NULL, 1 TSRMLS_CC)

/**
 * convert record to xml string
 */
static int pdl_record_to_xml_str(pdl_record_t **record_p, smart_str *buf TSRMLS_DC)
{
	const char	*xml_tpl;
	char		*time_str;
	char		*label_str;
	char		*msg_str;
	char		*file_str;
	char		*data_str;
	int			data_len;
	char		*tmp_str;
	int			tmp_len;
	size_t		new_len;
	
	pdl_record_t *record = *record_p;

	xml_tpl = "  <record>\n"
			  "    <time>%s</time>\n"
   			  "    <level>%s</level>\n"
   			  "    <label>%s</label>\n"
   			  "    <msg>%s</msg>\n"
   			  "    <file>%s</file>\n"
   			  "    <line>%ld</line>\n"
   			  "    <data><![CDATA[%s%s]]></data>\n"
   			  "  </record>\n";

	pdl_format_record_time(&time_str, record->time.sec, record->time.usec TSRMLS_CC);
	data_len = pdl_format_zval(&data_str, record->data, 1, 0, 0 TSRMLS_CC);

	HTML_SPECIAL_CHARS(label_str, new_len, record->label);
	HTML_SPECIAL_CHARS(msg_str, new_len, record->msg);
	HTML_SPECIAL_CHARS(file_str, new_len, record->file);

	tmp_len = spprintf(&tmp_str, 0, xml_tpl, time_str, pdl_level_name(record->level),
		label_str, msg_str, file_str, record->line, (data_len > 0 ? "\n" : ""), data_str);

	smart_str_appendl(buf, tmp_str, tmp_len);

	efree(time_str);
	efree(label_str);
	efree(msg_str);
	efree(file_str);
	efree(data_str);
	efree(tmp_str);

	return ZEND_HASH_APPLY_KEEP;
}

static int pdl_gen_xml_log_header(char **header, long date, int period TSRMLS_DC)
{
	const char	*format;
	char		*date_str;
	int			len;

	format = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
			 "<header>\n"
			 "  <copyright>Copyright (c) 2012, Zoujie Wu</copyright>\n"
			 "  <from>%s</from>\n"
			 "  <period>%d</period>\n"
			 "  <generate>PDLogger %s</generate>\n"
			 "</header>\n";

	date_str = php_format_date(ZEND_STRL("Y-m-d H:i:s"), date, 1 TSRMLS_CC);
	len = spprintf(header, 0, format, date_str, period, PDL_VERSION);
	efree(date_str);

	return len;
}

/**
 * format records to XML
 */
PDL_API zval* pdl_format_xml(TSRMLS_D)
{
	zval 		*xml;
	smart_str	buf = {0};
	char		*tmp_str;
	int			tmp_len;

	smart_str_append_const(&buf, "<records>\n");
	
	tmp_len = spprintf(&tmp_str, 0, "  <start>%ld+%ld</start>\n", PDL_G(start_time).sec, PDL_G(start_time).usec);
	smart_str_appendl(&buf, tmp_str, tmp_len);
	efree(tmp_str);
	
	zend_hash_apply_with_argument(&PDL_G(records), (apply_func_arg_t) pdl_record_to_xml_str, &buf TSRMLS_CC);
	
	smart_str_append_const(&buf, "</records>\n");

	smart_str_0 (&buf);
	MAKE_STD_ZVAL(xml);
	ZVAL_STRINGL(xml, buf.c, buf.len, 1);
	smart_str_free(&buf);

	return xml;
}

/**
 * generate text log header
 */
static int pdl_gen_text_log_header(char **header, long date, int period TSRMLS_DC)
{
	const char	*format;
	char		*date_str;
	int			len;

	format = "# Copyright (c) 2012, Zoujie Wu. Generated by PDLogger %s\n"
			 "#\n"
			 "#   Record from %s\n"
			 "#   Change log after %d sec(s)\n";

	date_str = php_format_date(ZEND_STRL("Y-m-d H:i:s"), date, 1 TSRMLS_CC);
	len = spprintf(header, 0, format, PDL_VERSION, date_str, period);
	efree(date_str);

	return len;
}

/**
 * convert record to text string
 */
static int pdl_record_to_text_str(pdl_record_t **record_p, smart_str *buf TSRMLS_DC)
{
	char *tmp_str;
	long tmp_len;
	char *time_str;
	char *data_str;
	int  data_len;
	pdl_record_t *record = *record_p;

	pdl_format_record_time(&time_str, record->time.sec, record->time.usec TSRMLS_CC);
	data_len = pdl_format_zval(&data_str, record->data, 1, PDL_FORMAT_INDENT, 0 TSRMLS_CC);

	tmp_len = spprintf(&tmp_str, 0, "[%s] [%s] [%s] %s @%s:%d\n%s", 
		time_str,
		pdl_level_name(record->level),
		record->label,
		record->msg,
		record->file,
		record->line,
		data_len > 0 ? data_str : ""
	);

	smart_str_appendl(buf, tmp_str, tmp_len);
	efree(time_str);
	efree(data_str);
	efree(tmp_str);

	return ZEND_HASH_APPLY_KEEP;
}

/**
 * format records to Text
 */
PDL_API zval* pdl_format_text(TSRMLS_D)
{
	zval		*text;
	char		*tmp_str;
	int			tmp_len;
	smart_str	buf = {0};

	/* add text log start flag */
	tmp_len = spprintf(&tmp_str, 0, "\n\n@start %ld+%ld\n", PDL_G(start_time).sec, PDL_G(start_time).usec);
	smart_str_appendl(&buf, tmp_str, tmp_len);
	efree(tmp_str);

	zend_hash_apply_with_argument(&PDL_G(records), (apply_func_arg_t) pdl_record_to_text_str, &buf TSRMLS_CC);

	smart_str_0 (&buf);
	MAKE_STD_ZVAL(text);
	ZVAL_STRINGL(text, buf.c, buf.len, 1);
	smart_str_free(&buf);

	return text;
}

static int pdl_gen_custom_log_header(char **header, long date, int period TSRMLS_DC)
{
	zval **params = NULL;
	zval *retval = NULL;
	int  header_len = 0;

	params = (zval **) emalloc(sizeof(zval *) * 2);

	MAKE_STD_ZVAL(retval);
	ZVAL_NULL(retval);

	MAKE_STD_ZVAL(params[0]);
	ZVAL_LONG(params[0], date);

	MAKE_STD_ZVAL(params[1]);
	ZVAL_LONG(params[1], period);

	call_user_function(EG(function_table), NULL, PDL_G(cf_cb).header_cb, retval, 2, params TSRMLS_CC);
	if (Z_TYPE_P(retval) == IS_STRING) {
		header_len = Z_STRLEN_P(retval);
		*header = estrndup(Z_STRVAL_P(retval), header_len);
	}

	zval_ptr_dtor(&retval);
	zval_ptr_dtor(&params[0]);
	zval_ptr_dtor(&params[1]);
	efree(params);

	return header_len;
}

static int pdl_record_to_custom_str(pdl_record_t **record_p, smart_str *buf TSRMLS_DC)
{
	zval			**params = NULL;
	zval			*retval = NULL;
	pdl_record_t	*record = *record_p;
	int				i;

	params = (zval **) emalloc(sizeof(zval *) * 7);

	MAKE_STD_ZVAL(params[0]);
	ZVAL_DOUBLE(params[0], (double)(record->time.sec + record->time.usec / MICRO_IN_SEC));

	MAKE_STD_ZVAL(params[1]);
	ZVAL_LONG(params[1], record->level);
	MAKE_STD_ZVAL(params[2]);
	ZVAL_STRING(params[2], record->label, 1);
	MAKE_STD_ZVAL(params[3]);
	ZVAL_STRING(params[3], record->msg, 1);

	if (record->data) {
		params[4] = record->data;
		Z_ADDREF_P(params[4]);
	} else {
		MAKE_STD_ZVAL(params[4]);
		ZVAL_NULL(params[4]);
	}

	MAKE_STD_ZVAL(params[5]);
	ZVAL_STRING(params[5], record->file, 1);
	
	MAKE_STD_ZVAL(params[6]);
	ZVAL_LONG(params[6], record->line);

	MAKE_STD_ZVAL(retval);
	ZVAL_NULL(retval);
	call_user_function(EG(function_table), NULL, PDL_G(cf_cb).record_cb, retval, 7, params TSRMLS_CC);
	if (Z_TYPE_P(retval) == IS_STRING) {
		smart_str_appendl(buf, Z_STRVAL_P(retval), Z_STRLEN_P(retval));
	}

	zval_ptr_dtor(&retval);
	for (i = 0; i < 7; i ++) {
		zval_ptr_dtor(&params[i]);
	}
	efree(params);
	
	return ZEND_HASH_APPLY_KEEP;
}

/**
 * format custom style log
 */
PDL_API zval* pdl_format_custom(TSRMLS_D)
{
	zval 		*custom = NULL;
	smart_str	buf = {0};

	if (PDL_G(cf_cb).delimiter) {
		smart_str_appendl(&buf, PDL_G(cf_cb).delimiter, PDL_G(cf_cb).delimiter_len);
	}

	zend_hash_apply_with_argument(&PDL_G(records), (apply_func_arg_t) pdl_record_to_custom_str, &buf TSRMLS_CC);

	smart_str_0 (&buf);
	MAKE_STD_ZVAL(custom);
	ZVAL_STRINGL(custom, buf.c, buf.len, 1);
	smart_str_free(&buf);

	return custom;
}

/**
 * generate log file
 */
static char* pdl_gen_log_file(zval* base_dir, int format TSRMLS_DC)
{
	const char	*ext = NULL;
	uint		ext_len;
	const char	*subdir = NULL;
	char 		*filename;
	char		*clean_dir;
	char		*parent_dir;
	int			tmp_len;
	long		tval;
	int			options;
	zval		*zexists;
	char		*header = "";
	int			header_len = 0;
	php_stream *stream;

	pdl_gen_log_header_t gen_header_func = NULL;

	switch (format) {
	case PDL_FM_CUSTOM:
		if (!PDL_G(cf_cb).header_cb) {
			return NULL;
		}
		ext = PDL_G(log_ext);
		subdir = "custom";
		gen_header_func = pdl_gen_custom_log_header;
		break;
	case PDL_FM_XML:
		ext = "xml";
		subdir = "xml";
		gen_header_func = pdl_gen_xml_log_header;
		break;
	case PDL_FM_TEXT:
		ext = "log";
		subdir = "text";
		gen_header_func = pdl_gen_text_log_header;
		break;
	}
	ext_len = strlen(ext);

	tval = (long) time(NULL);
	tval -= tval % PDL_G(period);

    MAKE_STD_ZVAL(zexists);
    ZVAL_BOOL(zexists, 0);

	/* generate parent dir name */
	clean_dir = php_trim(Z_STRVAL_P(base_dir), Z_STRLEN_P(base_dir), "/\\ ", 3, NULL, 2 TSRMLS_CC);
	tmp_len = strlen(clean_dir) + 1 + strlen(subdir) + 1;
	parent_dir = (char *) emalloc(sizeof(char) * (tmp_len + 1));
	sprintf(parent_dir, "%s/%s/", Z_STRVAL_P(base_dir), subdir);
	parent_dir[tmp_len] = 0;

    php_stat(parent_dir, (php_stat_len) tmp_len, FS_IS_DIR, zexists TSRMLS_CC);

	/* create parent dir */
	if (!Z_BVAL_P(zexists) && !php_stream_mkdir(parent_dir, 0777, PHP_STREAM_MKDIR_RECURSIVE | REPORT_ERRORS, NULL)) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to create log directory %s", parent_dir);
	}

	/* generate log file name */
	tmp_len += MAX_LENGTH_OF_LONG + (ext_len > 0 ? (1 + ext_len) : 0);
	filename = (char *) emalloc(sizeof(char) * (tmp_len + 1));
	sprintf(filename, "%s%ld%s%s", parent_dir, tval, (ext_len > 0 ? "." : ""), ext);
	filename[tmp_len] = '\0';

	php_stat(filename, (php_stat_len) tmp_len, FS_IS_FILE, zexists TSRMLS_CC);

	/* create log file */
	if (!Z_BVAL_P(zexists)) {
		options = ENFORCE_SAFE_MODE | REPORT_ERRORS | IGNORE_URL;
		stream = php_stream_open_wrapper(filename, "w", options, NULL);
		if (!stream) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to create file %s", filename);
		} else {
			if (gen_header_func) {
				header_len = gen_header_func(&header, tval, PDL_G(period) TSRMLS_CC);
				php_stream_write(stream, header, header_len);
				efree(header);
			}
			php_stream_close(stream);
		}
	}

	/* free resource */
	efree(clean_dir);
	efree(parent_dir);
    zval_ptr_dtor(&zexists);
	
	return filename;
}

/**
 * write records to output dirs as the format in configuration
 */
PDL_API void pdl_write_records(TSRMLS_D)
{
	zval			*output = NULL;
	pdl_record_t	*record;
	php_stream		*stream;
    zval			*dir, *dir_exists, *dir_writable;
    char 			*file = NULL;
    int				masks[] = {PDL_FM_CUSTOM, PDL_FM_XML, PDL_FM_TEXT};
    int				masks_count = 3;
    int				options, fm, i, elem_id;
    int				watch_status = (PDL_G(watch) & PDL_W_STATUS) && PDL_G(enable);

    if (!watch_status && zend_hash_num_elements(&PDL_G(records)) < 1) {
    	return;
    }

    MAKE_STD_ZVAL(dir);
    ZVAL_STRING(dir, PDL_G(output_dir), 1);
    
    MAKE_STD_ZVAL(dir_exists);
    ZVAL_BOOL(dir_exists, 0);

    MAKE_STD_ZVAL(dir_writable);
    ZVAL_BOOL(dir_writable, 0);

    php_stat(Z_STRVAL_P(dir), (php_stat_len) Z_STRLEN_P(dir), FS_IS_DIR, dir_exists TSRMLS_CC);
    php_stat(Z_STRVAL_P(dir), (php_stat_len) Z_STRLEN_P(dir), FS_IS_W, dir_writable TSRMLS_CC);

    if (!Z_BVAL_P(dir_exists)) {
    	php_error_docref(NULL TSRMLS_CC, E_WARNING, "The output dir is not exists, please create it first");
    } else if (!Z_BVAL_P(dir_writable)) {
    	php_error_docref(NULL TSRMLS_CC, E_WARNING, "The output dir is not writable, maybe you need to add the write permession");
    } else {
	    options = ENFORCE_SAFE_MODE | REPORT_ERRORS | IGNORE_URL;

		for (i = 0; i < masks_count; i ++) {
			fm = PDL_G(format) & masks[i];

			if (fm == 0 || ((fm == PDL_FM_CUSTOM) && !PDL_G(cf_cb).record_cb)) {
				continue;
			}

			if (watch_status) {
				record = pdl_get_watch_status(TSRMLS_C);
				elem_id = zend_hash_next_free_element(&PDL_G(records));
				zend_hash_index_update(&PDL_G(records), elem_id, &record, sizeof(pdl_record_t *), NULL);
			}

		    switch (fm) {
			case PDL_FM_CUSTOM:
				output = pdl_format_custom(TSRMLS_C);
				break;
			case PDL_FM_XML:
				output = pdl_format_xml(TSRMLS_C);
				break;
			case PDL_FM_TEXT:
				output = pdl_format_text(TSRMLS_C);
				break;
			default:
				php_error_docref(NULL TSRMLS_CC, E_ERROR, "Undefined log format %d", fm);
			}

			if (watch_status) {
				zend_hash_index_del(&PDL_G(records), elem_id);
			}

			/* do not write empty records */
			if (!output) {
				continue;
			}

			file = pdl_gen_log_file(dir, fm TSRMLS_CC);

			stream = php_stream_open_wrapper(file, "a", options, NULL);
			if (!stream) {
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to open log file %s", file);
			} else {
				php_stream_write(stream, Z_STRVAL_P(output), Z_STRLEN_P(output));
				php_stream_close(stream);
			}

			efree(file);
			zval_ptr_dtor(&output);
			output = NULL;
		}
    }

    zval_ptr_dtor(&dir);
    zval_ptr_dtor(&dir_exists);
    zval_ptr_dtor(&dir_writable);
}

/**
 * Iterator function for converting pdl_record_t to a array
 */
static int pdl_record_to_array(pdl_record_t **record_p, zval *aoa TSRMLS_DC)
{
	zval			*array;
	pdl_record_t	*record = *record_p;

	/* make a new array item */
	MAKE_STD_ZVAL(array);
	array_init(array);

	/* leve/time/label/msg */
	add_assoc_long(array, "level", record->level);
	add_assoc_double(array, "time", (double)(record->time.sec + record->time.usec / MICRO_IN_SEC));
	add_assoc_string(array, "label", record->label, 1);
	add_assoc_string(array, "msg", record->msg, 1);

	/* data */
	if (record->data) {
		add_assoc_zval(array, "data", record->data);
	} else {
		add_assoc_null(array, "data");
	}

	/* file/line */
	add_assoc_string(array, "file", record->file, 1);
	add_assoc_long(array, "line", record->line);

	/* put array item in aoa (array of array) */
	add_next_index_zval(aoa, array);

	return ZEND_HASH_APPLY_KEEP;
}

/**
 * export records as zval array
 */
PDL_API zval* pdl_export(TSRMLS_D)
{
	zval			*aoa;
	pdl_record_t	*record;

	MAKE_STD_ZVAL(aoa);
	array_init(aoa);

	zend_hash_apply_with_argument(&PDL_G(records), (apply_func_arg_t) pdl_record_to_array, aoa TSRMLS_CC);

	/* add status report at the end */
	if (PDL_G(watch) & PDL_W_STATUS) {
		record = pdl_get_watch_status(TSRMLS_C);
		pdl_record_to_array(&record, aoa TSRMLS_CC);
		pdl_record_dtor(&record);
	}

	return aoa;
}

/* {{{ proto long pdl_format([long $format])
 */
PHP_FUNCTION(pdl_format)
{
	long format = 0;
	
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &format) == FAILURE) {
		return;
	}
	
	RETVAL_LONG(PDL_G(format));
	if (format > 0) {
		PDL_G(format) = format;
	}
}
/* }}} */

/* {{{ proto bool pdl_format_handler(callable $record_cb[, callable $header_cb[, string $delimiter]])
 */
PHP_FUNCTION(pdl_format_handler)
{
	pdl_cf_cb_t	cf = {NULL, NULL};
	char		*handler_name = NULL;
	char		*delimiter = NULL;
	uint		delimiter_len = 0;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|z!s", &cf.record_cb, &cf.header_cb, &delimiter, &delimiter_len) == FAILURE) {
		return;
	}

	if (!zend_is_callable(cf.record_cb, 0, &handler_name TSRMLS_CC)) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s() is not a vaild callable function", handler_name);
		efree(handler_name);
		RETURN_FALSE;
	}
	efree(handler_name);

	if (cf.header_cb && !zend_is_callable(cf.header_cb, 0, &handler_name TSRMLS_CC)) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s() is not a vaild callable function", handler_name);
		efree(handler_name);
		RETURN_FALSE;
	}
	efree(handler_name);

	Z_ADDREF_P(cf.record_cb);
	if (cf.header_cb) {
		Z_ADDREF_P(cf.header_cb);
	}
	if (delimiter) {
		cf.delimiter = estrndup(delimiter, delimiter_len);
		cf.delimiter_len = delimiter_len;
	}

	PDL_CLEAN_CF_CB(PDL_G(cf_cb));

	PDL_G(cf_cb) = cf;

	RETURN_TRUE;
}
/* }}} */

/* {{{ proto array pdl_export()
 */
PHP_FUNCTION(pdl_export)
{
	zval *ret;
	if (return_value_used) {
		ret = pdl_export(TSRMLS_C);
		RETURN_ZVAL(ret, 0, 1);	/* do not copy but destroy */
	} else {
		RETURN_NULL();
	}
}
/* }}} */

/* {{{ proto void pdl_write()
 */
PHP_FUNCTION(pdl_write)
{
	if (!PDL_G(has_written)) {
		pdl_write_records(TSRMLS_C);
		PDL_G(has_written) = 1;
	} else {
		php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Unable to write records to log for twice more");
	}
}
/* }}} */

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */
